#!/usr/bin/env python

"""
<p>4sqNoGPS - This webapp is similar to Foursquare's mobile website but allows
manual input of coordinates. That will allow nearest venue searches and
check-ins by phones that do not have GPS.

<p>Input is optimized for handsets without a full keyboard by allowing coordinate entry using only digits. 4sqNoGPS streamlines the check-in process, making the default no-frills action single-click.

<p>4sqNoGPS uses Foursquare OAuth to log in, so it does not store user passwords. It is written in Python and is meant to be hosted on Google App Engine.

<pre>
Version: 0.0.10
Author: Po Shan Cheah (morton@mortonfox.com)
Source code: <a href="http://code.google.com/p/4sqnogps/">http://code.google.com/p/4sqnogps/</a>
Created: September 3, 2010
Last updated: December 6, 2010
</pre>
"""

from google.appengine.ext import webapp
from google.appengine.ext.webapp import util
from google.appengine.api import memcache
from google.appengine.ext import db
from google.appengine.api.urlfetch import DownloadError 

from django.utils import simplejson

import oauthConsumer
import pprint
import re
import sys
import StringIO
import os
import cgi
from math import (radians, sin, cos, atan2, degrees)
from datetime import (datetime, date, timedelta)
import urllib

CONSUMER_KEY = 'TXFBMMTXQEYY3NNBO1U4EBOSFIWZBRA0CTBOSZE31KVOJQHW'
CONSUMER_SECRET = '5CVBDUCHPUJPHPIFUEEZSCQ0HUKDV1OVNUCIANLNK4LY1URW'

TOKEN_COOKIE = '4sqnogpstoken'
TOKEN_PREFIX = 'token_4sqnogps_'

REQUEST_URL = 'https://foursquare.com/oauth/request_token'
ACCESS_URL = 'https://foursquare.com/oauth/access_token'
AUTHORIZE_URL = 'https://foursquare.com/oauth/authorize'

MEMCACHE_PREFIX = 'oauth_4sqnogps_'
API_HOST = 'api.foursquare.com'

DEFAULT_LAT = '39.7'
DEFAULT_LON = '-75.6'
COORDS_COOKIE = '4sqnogpscoords'
DEBUG_COOKIE = '4sqnogpsdebug'

METERS_PER_MILE = 1609.344

USER_AGENT = '4sqnogps:0.0.10 20101206'

# In development environment, use local callback.
CALLBACK_URL = 'https://4sqnogps.appspot.com/oauth'
if os.environ.get('SERVER_SOFTWARE','').startswith('Devel'):
    CALLBACK_URL = 'http://localhost:8080/oauth'

def escape(s):
    return cgi.escape(s, quote = True)

class AuthToken(db.Model):
    """
    A temporary auth token that we will use to authenticate a user with a
    third party website. (We need to store the data while the user visits
    the third party website to authenticate themselves.)

    This database is also used to store access secrets indexed by access
    tokens. We keep the data separate by prefixing the tokens differently.
    """
    token = db.StringProperty(required=True)
    secret = db.StringProperty(required=True)
    created = db.DateTimeProperty(auto_now_add=True)


class CoordsTable(db.Model):
    """
    A table that stores coords associated with each token.
    """
    token = db.StringProperty(required=True)
    coords = db.StringProperty(required=True)
    created = db.DateTimeProperty(auto_now_add=True)


def pprint_to_str(obj):
    """
    Pretty print to a string buffer then return the string.
    """
    sb = StringIO.StringIO()
    pp = pprint.pprint(obj, sb, 4)
    return sb.getvalue()

def debug_json(self, jsn):
    """
    Pretty-print a JSON response but only if debugging is turned on.
    """
    if get_debug(self):
	self.response.out.write('<pre>%s</pre>' % escape(pprint_to_str(jsn)))


def set_debug(self, debug):
    """
    Set the debug option cookie.
    """
    self.response.headers.add_header(
	    'Set-Cookie',
	    '%s=%s; expires=Fri, 31-Dec-2020 23:59:59 GMT'
	    % (DEBUG_COOKIE, debug))


def get_debug(self):
    """
    Get the debug setting from cookie. If cookie is not found,
    assume we are not in debug mode.
    """
    debug = self.request.cookies.get(DEBUG_COOKIE)
    if debug is None:
	return 0
    return int(debug)


def no_cache(self):
    """
    Turn off web caching so that the browser will refetch the page.
    Also set the user-agent header.
    """
    self.response.headers.add_header('Cache-Control', 'no-cache') 
    self.response.headers.add_header('User-Agent', USER_AGENT) 


def query_coords(self):
    """
    Run a GQL query to get the coordinates, if available.
    """
    token = self.request.cookies.get(TOKEN_COOKIE)
    if token is not None:
	return CoordsTable.gql('WHERE token=:1 LIMIT 1', token).get()


def set_coords(self, lat, lon):
    """
    Store the coordinates in our table.
    """
    result = query_coords(self)
    if result is None:
	token = self.request.cookies.get(TOKEN_COOKIE)
	if token is not None:
	    CoordsTable(token = token, coords = "%s,%s" % (lat, lon)).put()
    else:
	# Update existing record.
	result.coords = "%s,%s" % (lat, lon)
	db.put(result)


def coords(self):
    """
    Get user's coordinates from coords table. If not found in table,
    use default coordinates.
    """
    lat = None
    lon = None

    result = query_coords(self)
    if result is not None:
	try:
	    (lat, lon) = result.coords.split(',')
	except ValueError:
	    pass

    if lat is None or lon is None:
	lat = DEFAULT_LAT
	lon = DEFAULT_LON
	set_coords(self, lat, lon)

    return (lat, lon)


def newclient():
    """
    Create a new oauthConsumer client.
    """
    return oauthConsumer.Client(
	    key = CONSUMER_KEY,
	    secret = CONSUMER_SECRET,
	    requestTokenURL = REQUEST_URL,
	    accessTokenURL = ACCESS_URL,
	    authorizeURL = AUTHORIZE_URL,
	    callbackURL = CALLBACK_URL,
	    host = API_HOST,
	    useHttps = True)


def getclient(self):
    """
    Check if access token and secret cookies are available. If so, use them.
    Otherwise, do the OAuth handshake.
    """
    token = self.request.cookies.get(TOKEN_COOKIE)
    secret = None

    if token is not None:
	# Retrieve the access secret using the access token.
	result = AuthToken.gql("""
	    WHERE
		token = :1
	    LIMIT
		1
	    """, TOKEN_PREFIX + token).get()
	# If the query fails for whatever reason, the user will just
	# have to relogin. Not such a big deal.
	if result is not None:
	    secret = result.secret

    client = newclient()

    if token is None or secret is None:
	self.response.out.write('Not logged in.')
	self.redirect('/login')
	return

	authURL = client.requestAuth()

	# Save request secret in our database.
	auth = AuthToken(token = MEMCACHE_PREFIX + client.getRequestToken(),
		secret = client.getRequestSecret())
	auth.put()

	# Store the request secret in memcache too.
	memcache.set(MEMCACHE_PREFIX + client.getRequestToken(),
		client.getRequestSecret(),  time = 20 * 60)

	self.redirect(authURL)
	return

    client.setSession(token, secret)
    return client


def htmlbegin(self, title):
    self.response.out.write(
"""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>4sqNoGPS - %s</title>
<style type="text/css">
.error { color: red; background-color: white; }
</style>
</head>

<body>
<p><a href="/"><b>4sqNoGPS</b></a> - %s
""" % (title, title))

def htmlend(self, noabout=False, nologout=False):
    self.response.out.write("""
<hr>
<a href="/">Home</a>%s%s
</body>
</html>
""" % (
    '' if noabout else ' | <a href="/about">About</a>',
    '' if nologout else ' | <a href="/logout">Log out</a>'))


def conv_a_coord(coord, nsew):
    coord = float(coord)

    d = nsew[0]
    if coord < 0:
	d = nsew[1]
	coord = -coord

    return '%s%02d %06.3f' % (d, int(coord), 60 * (coord - int(coord)))

def convcoords(lat, lon):
    """
    Convert coordinates from decimal degrees to dd mm.mmm.
    Returns the result as a string.
    """
    return conv_a_coord(lat, 'NS') + ' ' + conv_a_coord(lon, 'EW')


def call4sq(self, client, method, path, params):
    """
    Call the Foursquare API. Handle errors.
    Returns None if there was an error. Otherwise, returns the parsed JSON.
    """
    try:
	req = client.createRequest(path=path)
	if method == 'post':
	    result = req.post(params=params)
	else:
	    result = req.get(params=params)

	jsn = simplejson.loads(result)

	error = jsn.get('error')
	if error is not None:
	    errorpage(self, error)
	    return

	error = jsn.get('ratelimited')
	if error is not None:
	    errorpage(self, '(rate limited) ' + error)
	    return

	error = jsn.get('unauthorized')
	if error is not None:
	    errorpage(self, '(unauthorized) ' + error)
	    return

	return jsn

    except DownloadError:
	errorpage(self,
		"Can't connect to Foursquare. #SadMayor Refresh to retry.")
	return


def userheader(self, client, lat, lon, badges=0, mayor=0):
    """ 
    Display the logged-in user's icon, name, and position.
    """
    jsn = call4sq(self, client, 'get', path='/v1/user.json',
	    params = { 
		'badges' : str(badges),
		'mayor' : str(mayor),
	    })
    if jsn is None:
	return

    user = jsn.get('user')
    if user is None:
	print >> sys.stderr, 'Bad response from /v1/user.json:'
	print >> sys.stderr, jsn
	return jsn

    usercheckin = user.get('checkin')
    if usercheckin is None:
	usertag = user['firstname']
    else:
	usertag = usercheckin['display']

    self.response.out.write(
	    '<p><img src="%s" style="float:left"> %s<br>Loc: %s'
	    '<br style="clear:both">' 
	    % (user['photo'], 
		escape(usertag), convcoords(lat, lon)))

    return jsn

def errorpage(self, msg):
    """
    Used for DownloadError exceptions. Generates an error page.
    """
    self.error(503)

    htmlbegin(self, "Error")
    self.response.out.write('<p><span class="error">Error: %s</span>' % msg)
    htmlend(self)


class LoginHandler(webapp.RequestHandler):
    """
    Page that we show if the user is not logged in.
    """
    def get(self):
	# This page should be cached. So omit the no_cache() call.
	htmlbegin(self, "Log in")

	self.response.out.write("""
<p>In order to use 4sqNoGPS features, you need to log in with Foursquare.
<p><a href="/login2">Log in with Foursquare</a>
""")
	htmlend(self, nologout=True)


class LoginHandler2(webapp.RequestHandler):
    """
    Second part of login handler. This does the actual login and redirection to
    Foursquare.
    """
    def get(self):
	self.response.out.write('Logging in to Foursquare...')

	client = newclient()

	authURL = client.requestAuth()

	# Save request secret in our database.
	auth = AuthToken(token = MEMCACHE_PREFIX + client.getRequestToken(),
		secret = client.getRequestSecret())
	auth.put()

	# Store the request secret in memcache too.
	memcache.set(MEMCACHE_PREFIX + client.getRequestToken(),
		client.getRequestSecret(),  time = 20 * 60)

	self.redirect(authURL)


class MainHandler(webapp.RequestHandler):
    """ 
    Main page for the app
    """
    def get(self):
	no_cache(self)

	(lat, lon) = coords(self)

	client = getclient(self)
	if client is None:
	    return

	htmlbegin(self, "Main")

	jsn = userheader(self, client, lat, lon)
	if jsn is None:
	    return

	leaderboard = 'http://foursquare.com/iphone/me?uid=%s' \
		% jsn['user']['id']

	self.response.out.write("""
<p>

<form style="margin:0; padding:0" action="/coords" method="get">
1. Enter coordinates: <input type="text" name="coords" size="8"
accesskey="1"><input type="submit" value="Go"></form>

2. <a href="/venues" accesskey="2">Nearest Venues</a><br>

<form style="margin:0; padding:0" action="/venues" method="get">
3. Search Venues: <input type="text" name="query" size="8"
accesskey="3"><input type="submit" value="Search"></form>

4. <a href="/history" accesskey="4">History</a><br>

5. <a href="/friends" accesskey="5">Find friends</a><br>

<form style="margin:0; padding:0" action="/shout" method="get">
6. Shout: <input type="text" name="message" size="8" accesskey="6">
<input type="submit" value="Shout"></form>

7. <a href="%s" accesskey="7">Leaderboard</a><br>

8. <a href="/badges" accesskey="8">Badges</a><br>

9. <a href="/mayor" accesskey="9">Mayorships</a><br>

10. <a href="/debug" accesskey="0">Turn debugging %s</a><br>

11. <a href="/geoloc">Detect location</a><br>

<p>Enter coordinates as a series of digits, e.g.:
<br>
<br>39123457512345 means N 39&deg; 12.345' W 75&deg; 12.345'
<br>391234751234 means N 39&deg; 12.340' W 75&deg; 12.340'
<br>3912375123 means N 39&deg; 12.300' W 75&deg; 12.300'
""" % (leaderboard, "off" if get_debug(self) else "on"))

	htmlend(self)

def checkin_badge_fmt(badge):
    return """
<p><img src="%s" style="float:left">
You've unlocked the %s badge: 
%s<br style="clear:both">
""" % (badge['icon'], badge['name'], badge['description'])

def checkin_score_fmt(score):
    return """
<p><img src="%s" style="float:left">
%s points: %s<br style="clear:both">
""" % (score['icon'], score['points'], score['message'])

def checkin_fmt(jsn):
    """
    Format checkin messages.
    """
    s = '<p>%s' % escape(jsn['checkin']['message'])

    venue = jsn['checkin'].get('venue')
    if venue is not None:
	s += '<p><a href="/venue?vid=%s">%s</a><br>%s' % ( 
		venue['id'], escape(venue['name']), addr_fmt(venue))

	# Add static Google Map to the page.
	s += google_map(venue['geolat'], venue['geolong'])

	pcat = venue.get('primarycategory')
	if pcat is not None:
	    s += category_fmt(pcat)

    mayor = jsn['checkin'].get('mayor')
    if mayor is not None:
	user = mayor.get('user')
	msg = escape(mayor['message'])
	s += '<p>%s' % msg if user is None else """
<p><img src="%s" style="float:left">%s<br style="clear:both">
""" % (user['photo'], msg)
    
    badges = jsn['checkin'].get('badges', [])
    s += ''.join([checkin_badge_fmt(b) for b in badges])

    scores = jsn['checkin'].get('scores', [])
    s += ''.join([checkin_score_fmt(score) for score in scores])
    
    s += specials_fmt(jsn['checkin'].get('specials', []))

    return s


class AddVenueHandler(webapp.RequestHandler):
    """
    Add a venue at the current coordinates with no address information.
    """

    # This is technically not idempotent but allow both methods anyway.
    def get(self):
	self.post()

    def post(self):
	no_cache(self)

	(lat, lon) = coords(self)
	client = getclient(self)
	if client is None:
	    return

	vname = self.request.get('vname')

	jsn = call4sq(self, client, 'post', path='/v1/addvenue.json',
		params = {
		    "name" : vname,
		    "geolat" : lat,
		    "geolong" : lon })
	if jsn is None:
	    return

	vid = str(jsn['venue']['id'])

	do_checkin(self, client, vid)


def do_checkin(self, client, vid):
    (lat, lon) = coords(self)

    jsn = call4sq(self, client, 'post', path='/v1/checkin.json',
	    params = {
		"vid" : vid,
		# "geolat" : lat,
		# "geolong" : lon
		})
    if jsn is None:
	return

    htmlbegin(self, "Check in")
    userheader(self, client, lat, lon)
    self.response.out.write(checkin_fmt(jsn))
    debug_json(self, jsn)
    htmlend(self)


class CheckinHandler(webapp.RequestHandler):
    """
    This handles user checkins by venue ID.
    """
    def get(self):
	no_cache(self)

	client = getclient(self)
	if client is None:
	    return

	vid = self.request.get('vid')

	do_checkin(self, client, vid)


class CheckinLong2Handler(webapp.RequestHandler):
    """
    Continuation of CheckinLongHandler after the user submits the
    checkin form with options.
    """
    def put(self):
	self.get()

    def get(self):
	no_cache(self)

	(lat, lon) = coords(self)
	client = getclient(self)
	if client is None:
	    return

	vid = self.request.get('vid')
	shout = self.request.get('shout')
	private = self.request.get('private')
	twitter = self.request.get('twitter')
	facebook = self.request.get('facebook')

	jsn = call4sq(self, client, 'post', path='/v1/checkin.json',
		params = {
		    'vid' : vid,
		    'shout' : shout,
		    'private' : private,
		    'twitter' : twitter,
		    'facebook' : facebook,
		    # 'geolat' : lat,
		    # 'geolong' : lon 
		    })
	if jsn is None:
	    return

	htmlbegin(self, "Check in")
	userheader(self, client, lat, lon)

	self.response.out.write(checkin_fmt(jsn))
	debug_json(self, jsn)
	htmlend(self)


class CheckinLongHandler(webapp.RequestHandler):
    """
    This handles user checkin with options.
    """
    def get(self):
	no_cache(self)

	(lat, lon) = coords(self)
	client = getclient(self)
	if client is None:
	    return

	vid = self.request.get('vid')
	vname = self.request.get('vname')

	htmlbegin(self, "Check in")

	jsn = userheader(self, client, lat, lon)
	if jsn is None:
	    return

	private = 1
	twitter = 0
	facebook = 0
	settings = jsn['user'].get('settings')
	if settings is not None:
	    if settings['pings'] == 'on':
		private = 0
	    if settings['sendtotwitter'] == 'true':
		twitter = 1
	    if settings['sendtofacebook'] == 'true':
		facebook = 1

	self.response.out.write('<p>Check in @ %s' % escape(vname))

	sel = 'selected="selected"'

	self.response.out.write("""
<form action="/checkin_long2" method="get">
Shout (optional): <input type="text" name="shout" size="15"><br>
<input type="hidden" value="%s" name="vid">
<input type="submit" value="check-in"><br>
<select name="private">
<option value="1" %s>Don't show your friends</option>
<option value="0" %s>Show your friends</option>
</select><br>
<select name="twitter">
<option value="0" %s>Don't send to Twitter</option>
<option value="1" %s>Send to Twitter</option>
</select><br>
<select name="facebook">
<option value="0" %s>Don't send to Facebook</option>
<option value="1" %s>Send to Facebook</option>
</select><br>
</form>
"""
	    % ( escape(vid), private and sel, private or sel,
		twitter or sel, twitter and sel,
		facebook or sel, facebook and sel ))

	debug_json(self, jsn)
	htmlend(self)

class ShoutHandler(webapp.RequestHandler):
    """
    This handles user shouts.
    """
    def put(self):
	self.get()

    def get(self):
	no_cache(self)
	(lat, lon) = coords(self)

	client = getclient(self)
	if client is None:
	    return

	message = self.request.get('message')

	jsn = call4sq(self, client, 'post', path='/v1/checkin.json',
		params = {
		    "shout" : message,
		    "geolat" : lat,
		    "geolong" : lon })
	if jsn is None:
	    return

	htmlbegin(self, "Shout")
	userheader(self, client, lat, lon)

	self.response.out.write('<p>%s' 
		% escape(jsn['checkin'].get('message', '')))

	debug_json(self, jsn)
	htmlend(self)


def deg_min(st):
    deg = st[:2]
    min = st[2:]
    if min == '':
	min = '0'
    if len(min) > 2:
	min = min[:2] + '.' + min[2:]
    return (deg, min)

def parse_coord(coordstr):
    """
    Parse user-entered coordinates.
    These coordinates are entered as digits only. The string is split into
    two halves. The first half fills in dd mm.mmm in the latitude and 
    the second half fills in dd mm.mmm in the longitude.
    """
    mid = int((len(coordstr) + 1) / 2)
    latstr = coordstr[:mid]
    lonstr = coordstr[mid:]

    (d, m) = deg_min(latstr)
    lat = "%.6f" % (int(d) + float(m) / 60)

    (d, m) = deg_min(lonstr)
    lon = "%.6f" % -(int(d) + float(m) / 60)

    return (lat, lon)

def isFloat(s):
    try:
	float(s)
	return True
    except ValueError:
	return False

class CoordsHandler(webapp.RequestHandler):
    """
    This handles user-input coordinates. Sets the location to 
    those coordinates and brings up the venue search page.
    """
    def get(self):
	self.post()

    def post(self):
	no_cache(self)

	htmlbegin(self, "Change location")

	geolat = self.request.get('geolat')
	geolong = self.request.get('geolong')

	# geolat/geolong are float parameters. Move to those coordinates.
	if isFloat(geolat) and isFloat(geolong):
	    set_coords(self, geolat, geolong)
	    self.redirect('/venues')
	    return

	coordinput = self.request.get('coords')

	# Extract digits. Ignore all other characters.
	instr = re.sub(r'[^0-9]', '', coordinput)

	if len(instr) >= 4:
	    (lat, lon) = parse_coord(instr)
	    set_coords(self, lat, lon)
	    self.redirect('/venues')
	else:
	    self.response.out.write(
		    '<p><span class="error">Bad input coords: %s</span>'
		    % escape(coordinput))

	htmlend(self)


def addr_fmt(venue):
    """
    Format the address block of a venue.
    """
    s = ''

    addr = venue.get('address', '')
    if addr != '':
	s += escape(addr) + '<br>'

    cross = venue.get('crossstreet', '')
    if cross != '':
	s += '(%s)<br>' % escape(cross)

    city = venue.get('city', '')
    state = venue.get('state', '')
    zip = venue.get('zip', '')
    if city != '' or state != '' or zip != '':
	s += '%s, %s %s<br>' % (escape(city), escape(state), escape(zip))

    phone = venue.get('phone', '')
    if len(phone) > 6:
	s += '(%s)%s-%s<br>' % (phone[0:3], phone[3:6], phone[6:])

    twitter = venue.get('twitter', '')
    if len(twitter) > 0:
	s += '<a href="http://mobile.twitter.com/%s">@%s</a><br>' % (
		urllib.quote(twitter), escape(twitter))

    return s


def venue_checkin_fmt(checkin, dnow):
    """
    Format the info about a user checked in at this venue.
    """
    s = ''
    s += '<p><img src="%s" style="float:left">%s %s from %s' % (
	    checkin['user']['photo'],
	    escape(checkin['user'].get('firstname', '')),
	    escape(checkin['user'].get('lastname', '')),
	    escape(checkin['user'].get('homecity', '')))

    shout = checkin.get('shout')
    if shout is not None:
	s += '<br>"%s"' % escape(shout)

    d1 = datetime.strptime(checkin['created'], '%a, %d %b %y %H:%M:%S +0000')
    s += '<br>%s' % fuzzy_delta(dnow - d1)

    s += '<br style="clear:both">'
    return s


def category_fmt(cat):
    return """
<p><img src="%s" style="float:left">%s
<br style="clear:both">
""" % (cat['iconurl'], cat['fullpathname'])


def venue_cmds(venue, checkin_long=False):
    """
    Show checkin/moveto links in venue header.
    """
    s = '<a href="/checkin?vid=%s">[checkin]</a>' % venue['id']
    if checkin_long:
	s += ' <a href="/checkin_long?%s">[checkin with options]</a>' % \
		escape(urllib.urlencode( { 
		    'vid' : venue['id'], 
		    'vname' : venue['name'].encode('utf-8')
		    } ))
    s += ' <a href="/coords?%s">[move to]</a>' % \
	    escape(urllib.urlencode( {
		'geolat' : venue['geolat'],
		'geolong' : venue['geolong']
		} ))
    # Link to venue page on Foursquare regular website.
    s += ' <a href="http://foursquare.com/venue/%s">[web]</a>' % venue['id']
    return s


def tip_fmt(tip):
    """
    Format a tip on the venue page.
    """
    return """
<p><img src="%s" style="float:left">%s %s from %s says: 
%s (Posted: %s)<br style="clear:both">
""" % (tip['user']['photo'],
	escape(tip['user'].get('firstname', '')),
	escape(tip['user'].get('lastname', '')),
	escape(tip['user'].get('homecity', '')),
	escape(tip['text']),
	tip['created'])

def tips_fmt(tips):
    """
    Format a list of tips on the venue page.
    """
    return '' if len(tips) == 0 else '<p><b>Tips:</b>' + ''.join(
	    [tip_fmt(t) for t in tips])


def google_map(lat, lon):
    """
    Static Google Map.
    """
    return """
<p><img width="150" height="150" alt="[Google Map]"
src="http://maps.google.com/maps/api/staticmap?%s">
""" % escape(urllib.urlencode( {
    'size' : '150x150', 
    'format' : 'gif',
    'sensor' : 'false',
    'zoom' : '14',
    'markers' : 'size:mid|color:blue|%s,%s' % (lat, lon),
    } ))


def vinfo_fmt(jsn):
    """
    Format info on a venue.
    """

    venue = jsn['venue']

    s = ''

    s += '<p>%s %s<br>%s' % (
	    escape(venue['name']), venue_cmds(venue, checkin_long=True),
	    addr_fmt(venue))

    # Add static Google Map to the page.
    s += google_map(venue['geolat'], venue['geolong'])

    cats = venue.get('categories', [])
    s += ''.join([category_fmt(c) for c in cats])

    tags = venue.get('tags', [])
    if len(tags) > 0:
	s += '<p>Tags: %s' % escape(', '.join(tags))

    stats = venue.get('stats')
    if stats is not None:
	s += """
<p>Checkins: %s <br>Here now: %s <br>Been here: %s
""" % (stats['checkins'], stats['herenow'], stats['beenhere']['me'])

	mayor = stats.get('mayor')
	s += '<p>No mayor' if mayor is None else """
<p><img src="%s" style="float:left">%s %s (%sx) 
from %s is the mayor<br style="clear:both"> 
""" % (mayor['user']['photo'], 
	escape(mayor['user'].get('firstname', '')), 
	escape(mayor['user'].get('lastname', '')), 
	mayor['count'], 
	escape(mayor['user'].get('homecity', '')))

    checkins = venue.get('checkins', [])
    if len(checkins) > 0:
	dnow = datetime.utcnow()
	s += '<p><b>Checked in here:</b>' + ''.join(
		[venue_checkin_fmt(c, dnow) for c in checkins])

    s += tips_fmt(venue.get('tips', []))
    s += specials_fmt(venue.get('specials', []))
    return s


def special_fmt(special):
    """
    Format a venue special.
    """
    s = ''
    venue = special.get('venue', {})

    pcat = venue.get('primarycategory')
    if pcat is not None:
	s += category_fmt(pcat)

    s += '<p>%s (%s, %s): %s' % (
	    escape(venue.get('name', '')), special['type'], special['kind'],
	    escape(special.get('message', '')))
    return s


def specials_fmt(specials):
    """
    Format venue specials.
    """
    return '' if len(specials) == 0 else '<p><b>Specials:</b>' + ''.join(
	    [special_fmt(x) for x in specials])


class VInfoHandler(webapp.RequestHandler):
    """
    This handler displays info on one venue.
    """
    def get(self):
	no_cache(self)

	(lat, lon) = coords(self)
	client = getclient(self)
	if client is None:
	    return

	vid = self.request.get('vid')

	jsn = call4sq(self, client, 'get', path='/v1/venue.json',
		params = { "vid" : vid })
	if jsn is None:
	    return

	htmlbegin(self, "Venue info")
	userheader(self, client, lat, lon)

	self.response.out.write(vinfo_fmt(jsn))

	debug_json(self, jsn)
	htmlend(self)

def bearing(lat, lon, vlat, vlon):
    """
    Compute compass direction from (lat, lon) to (vlat, vlon)
    """
    dlon = radians(float(vlon) - float(lon))
    lat1 = radians(float(lat))
    lat2 = radians(float(vlat))

    y = sin(dlon) * cos(lat2)
    x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dlon)
    brng = degrees(atan2(y, x))

    compass = [ 'S', 'SW', 'W', 'NW', 'N', 'NE', 'E', 'SE', 'S' ]
    return compass[int((brng + 180 + 22.5) / 45)]


def venue_fmt(venue, lat, lon):
    """
    Format a venue in the venue search page.
    """
    s = ''

    s += '<p><a href="/venue?vid=%s">%s</a> %s<br>%s' % (
	    venue['id'], escape(venue['name']), 
	    venue_cmds(venue), addr_fmt(venue))

    # Show distance and bearing from current coordinates.
    dist = venue.get('distance')
    if dist is not None:
	dist = float(dist) / METERS_PER_MILE
	compass = bearing(lat, lon, venue['geolat'], venue['geolong'])
	s += '(%.1f mi %s)<br>' % (dist, compass)

    return s


def remove_dup_vids(venues):
    """
    Return a new list of venues with all duplicate entries removed.
    """
    vids = []
    newvenues = []
    for v in venues:
	id = v['id']
	if id not in vids:
	    vids.append(id)
	    newvenues.append(v)
    return newvenues

def venues_fmt(jsn, lat, lon):
    """
    Format a list of venues in the venue search page.
    """

    groups = jsn.get('groups')
    if groups is None:
	return 'No matching venues found.'

    # Venues may be split across groups so collect them all in one list.
    venues = []
    for group in groups:
	venues.extend(group['venues'])

    venues = remove_dup_vids(venues)

    # Sort venues ascending by distance. If distance field is missing,
    # use a very large value.
    venues.sort(key = lambda v: v.get('distance', '1000000'))	  

    return ''.join([venue_fmt(v, lat, lon) for v in venues])


class VenuesHandler(webapp.RequestHandler):
    """
    Handler for venue search.
    """
    def post(self):
	self.get()

    def get(self):
	no_cache(self)

	(lat, lon) = coords(self)
	client = getclient(self)
	if client is None:
	    return

	# query is an optional keyword search parameter. If it is not present,
	# then just do a nearest venues search.
	query = self.request.get('query')

	parms = { "geolat" : lat, "geolong" : lon, "l" : "50" }
	if query != '':
	    parms['q'] = query

	jsn = call4sq(self, client, 'get', path='/v1/venues.json',
		params = parms)
	if jsn is None:
	    return

	htmlbegin(self, "Venue search")
	userheader(self, client, lat, lon)

	self.response.out.write("""
<form style="margin:0; padding:0" action="/addvenue" method="get"><p>
Add venue here and check in: <input type="text" name="vname" size="15"><input type="submit" value="Add Venue"></p></form>

<p>""" + venues_fmt(jsn, lat, lon))

	debug_json(self, jsn)
	htmlend(self)


def history_checkin_fmt(checkin, dnow):
    """
    Format an item from the check-in history.
    """
    s = ''

    venue = checkin.get('venue')
    if venue is not None:
	id = venue.get('id')
	# Orphaned venues will be missing the id field.
	if id is None:
	    s += '<p>%s<br>' % escape(venue['name'])
	else:
	    s += '<p><a href="/venue?vid=%s">%s</a> %s<br>%s' % (
		    id, escape(venue['name']), venue_cmds(venue),
		    addr_fmt(venue)
		    )

    shout = checkin.get('shout')
    if shout is not None:
	s += '"%s"<br>' % escape(shout)

    d1 = datetime.strptime(checkin['created'], '%a, %d %b %y %H:%M:%S +0000')
    s += fuzzy_delta(dnow - d1)

    return s


class HistoryHandler(webapp.RequestHandler):
    """
    Handler for history command.
    """
    def get(self):
	no_cache(self)

	(lat, lon) = coords(self)
	client = getclient(self)
	if client is None:
	    return

	jsn = call4sq(self, client, 'get', path='/v1/history.json',
		params = { 'l' : '50' })
	if jsn is None:
	    return

	htmlbegin(self, "History")
	userheader(self, client, lat, lon)

	checkins = jsn.get('checkins', [])
	dnow = datetime.utcnow()
	self.response.out.write(
	    '<p>No check-ins?' if len(checkins) == 0 else
	    ''.join(
		[history_checkin_fmt(checkin, dnow) for checkin in checkins]))

	debug_json(self, jsn)
	htmlend(self)

def mayor_venue_fmt(venue):
    return '<li><a href="/venue?vid=%s">%s</a> %s<br>%s' % (
	    venue['id'], escape(venue['name']), venue_cmds(venue),
	    addr_fmt(venue))

class MayorHandler(webapp.RequestHandler):
    """
    Handler for mayor command.
    """
    def get(self):
	no_cache(self)

	(lat, lon) = coords(self)
	client = getclient(self)
	if client is None:
	    return

	htmlbegin(self, "Mayorships")

	jsn = userheader(self, client, lat, lon, mayor=1)
	if jsn is None:
	    return

	mayorships = jsn['user'].get('mayor', [])
	self.response.out.write('<p>No mayorships yet.' 
		if len(mayorships) == 0 else 
		'<ol style="padding: 0 0 0 1.5em">%s</ol>' % 
		''.join([mayor_venue_fmt(v) for v in mayorships]))

	debug_json(self, jsn)
	htmlend(self)


def badge_fmt(badge):
    return """
<p><img src="%s" style="float:left"> %s<br>%s<br style="clear:both">
""" % (badge['icon'], badge['name'], badge['description'])

class BadgesHandler(webapp.RequestHandler):
    """
    Handler for badges command.
    """
    def get(self):
	no_cache(self)

	(lat, lon) = coords(self)
	client = getclient(self)
	if client is None:
	    return

	htmlbegin(self, "Badges")

	jsn = userheader(self, client, lat, lon, badges=1)
	if jsn is None:
	    return

	badges = jsn['user'].get('badges', [])
	self.response.out.write('<p>No badges yet.' if len(badges) == 0 else
		''.join([badge_fmt(b) for b in badges]))

	debug_json(self, jsn)
	htmlend(self)

def friend_checkin_fmt(checkin, lat, lon, dnow):
    """
    Format checkin record from one friend.
    """
    s = ''

    venue = checkin.get('venue')

    pdisp = escape(checkin['display'])
    if venue is not None:
	vid = venue.get('id')
	if vid is not None:
	    pdisp = '<a href="/venue?vid=%s">%s</a>' % (vid, pdisp)
    s += '<p>%s<br>' % pdisp

    shout = checkin.get('shout')
    if shout is not None:
	s += '"%s"<br>' % escape(shout)

    dist = checkin.get('distance')
    if dist is not None:
	dist = float(dist) / METERS_PER_MILE

    if venue is not None:
	s += addr_fmt(venue)

	geolat = venue.get('geolat')
	geolong = venue.get('geolong')
	
	if geolat is None or geolong is None:
	    compass = ''
	else:
	    compass = bearing(lat, lon, geolat, geolong)

	if dist is not None:
	    s += '(%.1f mi %s)<br>' % (dist, compass)
    else:
	if dist is not None:
	    s += '(%.1f mi)<br>' % dist

    d1 = datetime.strptime(checkin['created'], '%a, %d %b %y %H:%M:%S +0000')
    s += fuzzy_delta(dnow - d1)

    return s


def fuzzy_delta(delta):
    """
    Returns a user-friendly version of timedelta.
    """
    if delta.days < 0:
	return 'in the future?'
    elif delta.days > 1:
	return '%d days ago' % delta.days
    elif delta.days == 1:
	return '1 day ago'
    else:
	hours = int(delta.seconds / 60 / 60)
	if hours > 1:
	    return '%d hours ago' % hours
	elif hours == 1:
	    return '1 hour ago'
	else:
	    minutes = int(delta.seconds / 60)
	    if minutes > 1:
		return '%d minutes ago' % minutes
	    elif minutes == 1:
		return '1 minute ago'
	    else:
		if delta.seconds > 1:
		    return '%d seconds ago' % delta.seconds
		elif delta.seconds == 1:
		    return '1 second ago'
		else:
		    return 'now'


class FriendsHandler(webapp.RequestHandler):
    """
    Handler for Find Friends command.
    """
    def get(self):
	no_cache(self)

	(lat, lon) = coords(self)
	client = getclient(self)
	if client is None:
	    return

	jsn = call4sq(self, client, 'get', path='/v1/checkins.json',
		params = { 'geolat':lat, 'geolong':lon })
	if jsn is None:
	    return

	htmlbegin(self, "Find Friends")
	userheader(self, client, lat, lon)

	dnow = datetime.utcnow()

	checkins = jsn.get('checkins', [])

	# Sort checkins by distance. If distance is missing,
	# use a very large value.
	checkins.sort(key = lambda v: v.get('distance', '1000000'))	  

	self.response.out.write('<p>No friends?' if len(checkins) == 0 else
	    ''.join(
		[friend_checkin_fmt(c, lat, lon, dnow) for c in checkins]))

	debug_json(self, jsn)
	htmlend(self)


class LogoutHandler(webapp.RequestHandler):
    """
    Handler for user logout command.
    """
    def del_cookie(self, cookie):
	""" 
	Delete cookies by setting expiration to a past date.
	"""
	self.response.headers.add_header(
		'Set-Cookie', 
		'%s=; expires=Fri, 31-Dec-1980 23:59:59 GMT' % cookie)

    def get(self):
	# This page should be cached. So omit the no_cache() call.

	self.del_cookie(TOKEN_COOKIE)
	self.del_cookie(COORDS_COOKIE)

	htmlbegin(self, "Logout")
	self.response.out.write('<p>You have been logged out')
	htmlend(self, nologout=True)


class AboutHandler(webapp.RequestHandler):
    """
    Handler for About command.
    """
    def get(self):
	# This page should be cached. So omit the no_cache() call.
	htmlbegin(self, "About")
	self.response.out.write(__doc__)
	htmlend(self, noabout=True, nologout=True)


class DebugHandler(webapp.RequestHandler):
    """
    Handler for Debug command. Toggle debug mode.
    """
    def get(self):
	debug = get_debug(self)
	set_debug(self, (0 if debug else 1))
	self.redirect('/')


class GeoLocHandler(webapp.RequestHandler):
    """
    Geolocation Handler. Will attempt to detect location using HTML5
    Geolocation API and set our coordinates accordingly.
    """
    def get(self):
	# This page should be cached. So omit the no_cache() call.
	htmlbegin(self, "Detect Location")
	self.response.out.write("""
<noscript>
<p><span class="error">No Javascript support or Javascript disabled.</span> Can't detect location.
</noscript>
<p><span id="output">&nbsp;</span>
<script type="text/javascript">
function show(msg) {
    var out = document.getElementById('output');
    out.innerHTML = msg;
}

function error(msg) {
    show('<span class="error">' + msg + '</span>');
}

function error_callback(err) {
    switch (err.code) {
    case err.PERMISSION_DENIED:
	error('No permission to get location: ' + err.message);
	break;
    case err.POSITION_UNAVAILABLE:
	error('Could not get location: ' + err.message);
	break;
    case err.TIMEOUT:
	error('Network timeout: ' + err.message);
	break;
    default:
	error('Unknown error: ' + err.message);
	break;
    }
}

function success_callback(pos) {
    show('Detected coordinates: ' + 
	pos.coords.latitude + ',' + pos.coords.longitude);
    // Redirect to our coordinates handler once we have the info.
    window.location = '/coords?geolat=' + pos.coords.latitude + 
	'&geolong=' + pos.coords.longitude
}

if (navigator.geolocation) {
    show('Detecting location...');
    navigator.geolocation.getCurrentPosition(
	success_callback, error_callback, { timeout: 30000 });
}
else {
    error('Geolocation API not supported in this browser.')
}
</script>
""")

	htmlend(self)


class PurgeHandler(webapp.RequestHandler):
    """
    Purge old database entries from CoordsTable and AuthToken.
    """
    def get(self):
	no_cache(self)

	cutoffdate = (date.today() - timedelta(days=30)).isoformat()
	creatclause = "WHERE created < DATE('%s')" % cutoffdate

	htmlbegin(self, 'Purge old database entries')

	query = AuthToken.gql(creatclause)
	count = 0
	for result in query:
	    result.delete()
	    count += 1
	self.response.out.write('<p>Deleted %d old entries from AuthToken table' % count)

	query = CoordsTable.gql(creatclause)
	count = 0
	for result in query:
	    result.delete()
	    count += 1
	self.response.out.write('<p>Deleted %d old entries from CoordsTable table' % count)

	htmlend(self)


class OAuthHandler(webapp.RequestHandler):
    """
    This handler is the callback for the OAuth handshake. It stores the access
    token and secret in cookies and redirects to the main page.
    """
    def get(self):
	no_cache(self)

	token = self.request.get('oauth_token')
	verifier = self.request.get('oauth_verifier')

# 	self.response.out.write('<p>token = '+token)
# 	self.response.out.write('<p>verifier = '+verifier)

	client = newclient()

	# Retrieve request secret from memcache.
	secret = memcache.get(MEMCACHE_PREFIX + token)

	if secret is None:

	    # If not in memcache, try getting it from the db.
	    result = AuthToken.gql("""
		WHERE
		  token = :1
		LIMIT
		  1
		""", MEMCACHE_PREFIX + token).get()
	    if result is None:
		self.response.out.write(
			'The auth token %s was not found in our db' 
			% escape(token))
		return
	    secret = result.secret

# 	self.response.out.write('<p>secret = '+secret)
	client.setRequestSession(token, secret)

	sessionInfo = client.requestSession(token, verifier)

# 	self.response.out.write('<p>'+sessionInfo)
# 	self.response.out.write('<p>token = '+client._sessionToken)
# 	self.response.out.write('<p>secret = '+client._sessionSecret)

	# Store only the token in the cookie. This is so that no one
	# can hijack the access secret by intercepting http packets.
	self.response.headers.add_header(
		'Set-Cookie', 
		'%s=%s; expires=Fri, 31-Dec-2020 23:59:59 GMT' \
			% (TOKEN_COOKIE, client.getSessionToken()))

	# Add the secret to the database.
	auth = AuthToken(token = TOKEN_PREFIX + client.getSessionToken(),
		secret = client.getSessionSecret())
	auth.put()

	self.redirect('/')


def main():
    application = webapp.WSGIApplication(
	    [
		('/', MainHandler),
		('/oauth', OAuthHandler),
		('/logout', LogoutHandler),
		('/venues', VenuesHandler),
		('/coords', CoordsHandler),
		('/checkin', CheckinHandler),
		('/checkin_long', CheckinLongHandler),
		('/checkin_long2', CheckinLong2Handler),
		('/addvenue', AddVenueHandler),
		('/venue', VInfoHandler),
		('/badges', BadgesHandler),
		('/mayor', MayorHandler),
		('/friends', FriendsHandler),
		('/shout', ShoutHandler),
		('/history', HistoryHandler),
		('/debug', DebugHandler),
		('/about', AboutHandler),
		('/geoloc', GeoLocHandler),
		('/login', LoginHandler),
		('/login2', LoginHandler2),
		('/purge', PurgeHandler),
	    ],
	    debug=True)
    util.run_wsgi_app(application)


if __name__ == '__main__':
    main()

# vim:set tw=0:
